package com.atguigu.gulimall.product.service.impl;

import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.vo.Catalog2Vo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    ObjectMapper objectMapper;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        List<CategoryEntity> categoryEntities = this.baseMapper.selectList(null);
        // 找出顶级分类
        return categoryEntities.stream().filter(c -> c.getParentCid().equals(0L))
                .peek(c -> {
                    c.setChildren(getChildren(c, categoryEntities));
                }).sorted(Comparator.comparingInt(CategoryEntity::getSort))
                .collect(Collectors.toList());
    }

    @Override
    public List<Long> getCatelogPath(Long catelogId) {
        ArrayList<Long> paths = new ArrayList<>();
        findParentCatalogId(catelogId, paths);
        Collections.reverse(paths);
        return paths;
    }

    /**
     * spirng-cahce 不足：
     * 1. 读模式：
     * 缓存穿透：查询一个null数据。解决：缓存空数据，cache-null-values: true
     * 缓存击穿：大量并发进来同时查询一个正好过期的数据。解决：加锁。默认是不加锁，，修改 ,sync = true
     * 缓存雪崩：大量的key同时过期。解决。加上过期时间：time-to-live: 60s
     * 2. 写模式（缓存与数据库一致）
     * 1、读写加锁
     * 2、引入canal，感知到myql的更新就去更新缓存数据库
     * 3、读多写多，直接去数据库查询就行
     * 总结：
     * 常规数据（读多写少，及时性、一致性要求不高的数据） 完全可以使用spring-cache，写模式（设置过期时间）
     * 特殊数据：特殊设计处理。
     *
     * @param category
     */
    //    @Caching(evict = {
//            @CacheEvict(value = {"category"}, key = "'selectLevel1Categories'" ),
//            @CacheEvict(value = {"category"}, key = "'getCatalogJson'" )
//    })
    @CacheEvict(value = "category", allEntries = true)
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.baseMapper.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

    @Cacheable(value = {"category"}, key = "#root.method.name")
    @Override
    public List<CategoryEntity> selectLevel1Categories() {
        System.out.println("查询了数据库");
        return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    }

    @Cacheable(value = {"category"}, key = "#root.method.name", sync = true)
//    @CachePut // 双写，总是保存数据到缓存
    @Override
    public Map<String, List<Catalog2Vo>> getCatalogJson() throws IOException, InterruptedException {
        /**
         * 1.空结果缓存：缓存穿透
         * 2.设置过期时间（随机值）：缓存雪崩
         * 3.加锁：解决缓存击穿。
         */
        String data = stringRedisTemplate.opsForValue().get("catalog-json");
        if (StringUtils.isEmpty(data)) {
            Map<String, List<Catalog2Vo>> catalogJson = getCatalogJsonFromDBWithRedisson();
            // 数据保存到缓存中
            // todo 单个锁 ReentrantLock reentrantLock = new ReentrantLock();

            // todo 2 分布式锁


            return catalogJson;
        }
        return objectMapper.readValue(data, new TypeReference<Map<String, List<Catalog2Vo>>>() {
        });

    }

    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithRedisson() throws IOException, InterruptedException {

        Map<String, List<Catalog2Vo>> data;

        // 分布式锁
        RLock lock = redissonClient.getLock("lock:catalog-json");

        try {
            lock.lock();
            String raw = stringRedisTemplate.opsForValue().get("catalog-json");
            if (!StringUtils.isEmpty(raw)) {
                data = objectMapper.readValue(raw, new TypeReference<Map<String, List<Catalog2Vo>>>() {
                });
                System.out.println("redis中已存在当前数据 " + Thread.currentThread().getName());
                return data;
            }
            data = getCatalogJsonFromDB();
            stringRedisTemplate.opsForValue().set("catalog-json", objectMapper.writeValueAsString(data), 1, TimeUnit.HOURS);
        } finally {
            lock.unlock();
        }


        return data;
    }

    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithRedis() throws IOException, InterruptedException {
        String raw = stringRedisTemplate.opsForValue().get("catalog-json");
        Map<String, List<Catalog2Vo>> data;
        if (!StringUtils.isEmpty(raw)) {
            data = objectMapper.readValue(raw, new TypeReference<Map<String, List<Catalog2Vo>>>() {
            });
            System.out.println("redis中已存在当前数据 " + Thread.currentThread().getName());
            return data;
        }
        String uuid = UUID.randomUUID().toString();
        Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(locked)) {
            // 获得了分布式锁
            System.out.println("获得了分布式锁 " + Thread.currentThread().getName());
            data = getCatalogJsonFromDB();
            stringRedisTemplate.opsForValue().set("catalog-json", objectMapper.writeValueAsString(data), 1, TimeUnit.HOURS);
            // 解锁
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class) {
            }, Collections.singletonList("lock"), uuid);
        } else {
            // 自旋获取redis锁
            System.out.println("自旋获取redis锁 " + Thread.currentThread().getName());
            Thread.sleep(200);
            return this.getCatalogJsonFromDBWithRedis();
        }

        return data;
    }


    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDB() {
        System.out.println("正在查询数据库 " + Thread.currentThread().getName());
        List<CategoryEntity> list = this.list();
        Map<String, List<CategoryEntity>> dataMap = list.stream().collect(Collectors.groupingBy(v -> v.getParentCid().toString()));

        List<CategoryEntity> l1List = dataMap.get("0");
        Map<String, List<Catalog2Vo>> catalogData = l1List.stream().collect(Collectors.toMap(k -> k.getCatId().toString(),
                l1 -> {
                    List<Catalog2Vo> l2List = dataMap.get(l1.getCatId().toString())
                            .stream().map(l2 -> {
                                Catalog2Vo catalog2Vo = new Catalog2Vo();
                                catalog2Vo.setCatalog1Id(l1.getCatId().toString());
                                catalog2Vo.setId(l2.getCatId().toString());
                                catalog2Vo.setName(l2.getName());
                                List<Catalog2Vo.Catalog3Vo> l3List = dataMap.get(l2.getCatId().toString()).stream().map(l3 -> {
                                    Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo();
                                    catalog3Vo.setCatalog2Id(l2.getCatId().toString());
                                    catalog3Vo.setId(l3.getCatId().toString());
                                    catalog3Vo.setName(l3.getName());
                                    return catalog3Vo;
                                }).collect(Collectors.toList());
                                catalog2Vo.setCatalog3List(l3List);
                                return catalog2Vo;
                            }).collect(Collectors.toList());
                    return l2List;
                }
        ));


        return catalogData;
    }

    private List<Long> findParentCatalogId(Long catelogId, List<Long> paths) {
        // 收集当前节点
        paths.add(catelogId);
        Long parentCid = this.baseMapper.selectById(catelogId).getParentCid();
        if (parentCid != 0) {
            findParentCatalogId(parentCid, paths);
        }
        return paths;
    }

    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> entityList) {
        return entityList.stream()
                .filter(c -> c.getParentCid().equals(root.getCatId()))
                .peek(c -> {
                    if (c.getSort() == null)
                        c.setSort(0);
                    c.setChildren(getChildren(c, entityList));
                })
                .sorted(Comparator.comparingInt(CategoryEntity::getSort))
                .collect(Collectors.toList());
    }
}